package gulimall.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import gulimall.entity.Category;
import gulimall.mapper.CategoryMapper;
import gulimall.service.CategoryBrandRelationService;
import gulimall.service.ICategoryService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import gulimall.vo.web.Catelog2Vo;
import gulimall.vo.web.Catelog3Vo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * <p>
 * 商品三级分类 服务实现类
 * </p>
 *
 * @author HLF
 * @since 2022-04-23
 */
@Service
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

    /**
     * baseMapper就是categoryMapper
     */
    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private CategoryBrandRelationService categoryBrandRelationService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource(name = "redisTemplate")
    private ValueOperations<String, Object> valueOps;

    @Resource
    private RedissonClient redissonClient;

    @Override
    public List<Category> listWithTree() {
        //1.查询出所有的分类
        List<Category> categories = categoryMapper.selectList(null);
        //2.封装成为父子的树形结构
        //2.1查询出所有一级分类
        List<Category> level1Menus = categories.stream()
                .filter(category -> category.getCatLevel() == 1)
                //2.3调用map方法(需要手动设置返回值)设置Children，peek()不用设置返回值
                .map(category -> {
                    //2.4调用查询子分类的方法并设置子分类
                    category.setChildren(getChildrens(category, categories));
                    return category;
                })
                .sorted(Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))
                .collect(Collectors.toList());


        /*QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("cat_level", 1);
        List<Category> level1Menus = categoryMapper.selectList(queryWrapper);*/
        /**
         * 调用递归方法代替以下代码
         * @version 1.0
         */
        //2.2查询出所有二级分类，并赋值给一级分类的children
        /*for (Category level1Menu : level1Menus) {
            List<Category> level2Menus = categories.stream()
                    .filter(category -> category.getParentCid() == level1Menu.getCatId())
                    .collect(Collectors.toList());
            level1Menu.setChildren(level2Menus);
            for (Category level2Menu : level2Menus) {
                List<Category> level3Menus = categories.stream()
                        .filter(category -> category.getParentCid() == level2Menu.getCatId())
                        .collect(Collectors.toList());
                level2Menu.setChildren(level3Menus);
            }
        }*/

        return level1Menus;
    }

    /**
     * 递归查询多级菜单分类
     *
     * @param root 当前分类
     * @param all  父级分类
     * @return 返回分类的子分类
     * @version 2.0
     */
    private List<Category> getChildrens(Category root, List<Category> all) {
        return all.stream()
                //找出当前分类的所有子分类
                .filter(category -> Objects.equals(root.getCatId(), category.getParentCid()))
                .map(category -> {
                    category.setChildren(getChildrens(category, all));
                    return category;
                })
                /**
                 * .sorted((o1, o2) -> {
                 *    return (o1.getSort()==null?0:o1.getSort()) - (o2.getSort()==null?0:o2.getSort())
                 *  })
                 */
                .sorted(Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))
                .collect(Collectors.toList());
    }

    /**
     * 通过分类id数组批量逻辑删除分类，删除分类前要删除引用该分类的地方
     *
     * @param catIds
     * @return 返回删除成功的行数
     */
    @Override
    @Transactional
    public int removeMenusByIds(Long[] catIds) {
        //TODO 1.删除分类前要删除引用该分类的地方

        //2.逻辑删除
        return baseMapper.deleteBatchIds(Arrays.asList(catIds));
    }

    /**
     * 通过叶子节点的catId获取所有父节点的catId集合(获取节点的完整路径)
     *
     * @param catId
     * @return 返回当前节点以及所有父节点的catId的集合
     * ❗️注意顺序为：父节点在前，叶子节点在最后
     */
    @Override
    public List<Long> getCatIdsByLeaf(Long catId) {
        List<Long> catIds = new ArrayList<>();
        List<Long> allCIds = getParentCid(catId, catIds);
        Collections.reverse(allCIds);//按照 [顶级父节点，父节点，叶子节点] 的顺序
        return allCIds;
    }

    private List<Long> getParentCid(Long catId, List<Long> catIds) {
        catIds.add(catId);
        Category category = categoryMapper.selectById(catId);
        if (category.getParentCid() != 0) {
            getParentCid(category.getParentCid(), catIds);
        }
        return catIds;
    }

    /**
     * 修改分类，同时级联更新其他表中的引用
     * <p>
     *
     * @CacheEvict 失效模式
     * 1、同时进行多种缓存操作 @Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
     * 3、存储同一类型的数据，都可以指定成同一分区，分区名默认就是缓存的前缀
     */
    @Override
    @Transactional
    //@CacheEvict(value = {"category"}, key = "'listLevel1'")  //更新分类时删除缓存，等到下一次查询分类再重新从数据库中查，然后放入缓存
    @Caching(evict = {
            @CacheEvict(value = {"category"}, key = "'listLevel1'"),
            @CacheEvict(value = {"category"}, key = "'getCategoryJson'")
    })
    //加上读写锁
    public boolean updateCascader(Category category) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("category-updateCascader");
        RLock writeLock = readWriteLock.writeLock();
        int i = 0;
        try {
            boolean tryLock = writeLock.tryLock();
            if (tryLock) {
                //更新自身表
                i = categoryMapper.updateById(category);

                //如果分类名非空
                if (StringUtils.isNotBlank(category.getName())) {
                    //级联更新关联表pms_category_brand_relation中的字段
                    int j = categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());

                    //TODO 更新其他表
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            writeLock.unlock();
        }
        return i > 0;
    }

    /**
     * 查询所有的一级分类
     * <p>
     * 1、每一个需要缓存的数据我们都需要指定放到那个名字的缓存【缓存分区的划分【按照业务类型划分】】
     * 2、@Cacheable({"category"})
     * 代表当前方法的结果需要缓存，如果缓存中有，方法不调用
     * 如果缓存中没有，调用方法，最后将方法的结果放入缓存
     * 3、默认行为:
     * 1、如果缓存中有，方法不用调用
     * 2、key默认自动生成：缓存的名字:SimpleKey[](自动生成的key值)，例如：  category:SimpleKey[]
     * 3、缓存中value的值，默认使用jdk序列化，将序列化后的数据存到redis
     * 3、默认的过期时间，-1(永不过期)
     * <p>
     * 自定义操作
     * 1、指定缓存使用的key     key属性指定，接收一个SpEl
     * 2、指定缓存数据的存活时间  配置文件中修改ttl
     * 3、将数据保存为json格式
     * <p>
     * 4、Spring-Cache的不足：
     * 1、读模式：
     * 缓存穿透:查询一个null数据，解决 缓存空数据：ache-null-values=true
     * <p>
     * 缓存击穿:大量并发进来同时查询一个正好过期的数据，解决:加锁 , 默认是无加锁(可以手动加锁,也可以用sync)
     *
     * @Cacheable(value = {"category"},key = "#root.methodName",sync = true) 读模式下加了本地锁，		 			 snchronized粒度大。写模式无任何锁
     * 没有使用锁, 没办法解决缓存击穿问题. 不过@Cacheable中有个属性sync=true时, 可以加上本地锁, 没办法使用分布式锁. 			不过这种情况不是特别大型的项目, 每个服务的实例个数不是那么多的情况下, 影响也不大.
     * <p>
     * 缓存雪崩:大量的key同时过期，解决：加上随机时间，Spring-cache-redis-time-to-live
     * <p>
     * 2、写模式：（缓存与数据库库不一致）
     * 1、读写加锁
     * 2、引入canal，感知到MySQL的更新去更新数据库
     * 3、读多写多，直接去数据库查询就行
     * <p>
     * 总结：
     * 1.数据（读多写少，即时性，一致性要求不高的数据）完全可以使用SpringCache, 写模式（ 只要缓存数据有过期时间就足够了）
     * 2.写少的一般数据, 完全可以用spring cache, 一些特殊的数据, 要求实时性比较高的, 又要加锁的, 可以用分布式读写锁.
     * <p>
     * 特殊数据：特殊设计
     * 原理：
     * CacheManager(RedisManager) -> Cache(RedisCache) ->Cache负责缓存的读写
     */
    //value 缓存的别名
    //key   redis中key的名称
    //@Cacheable(value = {"category"}, key = "'listLevel1'") //普通字符串用单引号括起来
    @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)//key = "#root.method.name"  这俩一样
    @Override
    public List<Category> listLevel1() {

        return baseMapper.selectList(new QueryWrapper<Category>().lambda().eq(Category::getParentCid, 0L));
    }

    /**
     * 提取公共方法，优化
     * 获取子分类集合
     */
    private List<Category> getChildren(List<Category> categories, Long parentCid) {
        /*return categoryMapper.selectList(new QueryWrapper<Category>().lambda()
                .eq(Category::getParentCid, category1.getCatId()));*/

        return categories.stream().filter(category -> {
            return Objects.equals(category.getParentCid(), parentCid);
        }).collect(Collectors.toList());
    }

    /**
     * 提取公共方法：从数据库中查询数据，其中还会从Redis中查询一次
     *
     * @return
     */
    @NotNull
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        Map<String, List<Catelog2Vo>> categoryJson = (Map<String, List<Catelog2Vo>>) valueOps.get("categoryJson");
        if (categoryJson != null && categoryJson.size() > 0) {
            System.out.println("getDataFromDb()从redis中获取到数据！");
            return categoryJson;
        }

        /**
         * 优化：
         * 1.将数据库的多次查询变为一次
         */
        List<Category> categories = this.baseMapper.selectList(null);
        System.out.println("getCategoryJsonFromDb()从mysql中获取到数据！");


        //1.查询出所有一级分类
        List<Category> listLevel1 = getChildren(categories, 0L);

        //2.根据查询一级分类对应的二级和三级分类，并设置vo
        Map<String, List<Catelog2Vo>> map = listLevel1.stream().collect(Collectors.toMap(category1 -> category1.getCatId().toString(),
                category1 -> {
                    //查询一级分类对应的所有二级分类
                    List<Category> listLevel2 = getChildren(categories, category1.getCatId());
                    List<Catelog2Vo> catelog2Vos = listLevel2.stream().map(category2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo();
                        catelog2Vo.setCatalog1Id(String.valueOf(category1.getCatId()));
                        catelog2Vo.setName(category2.getName());
                        catelog2Vo.setId(String.valueOf(category2.getCatId()));
                        //查询二级分类对应的所有三级分类
                        List<Category> listLevel3 = getChildren(categories, category2.getCatId());
                        List<Catelog3Vo> catelog3Vos = listLevel3.stream().map(category3 -> {
                            Catelog3Vo catelog3Vo = new Catelog3Vo();
                            catelog3Vo.setName(category3.getName());
                            catelog3Vo.setCatalog2Id(String.valueOf(category2.getCatId()));
                            catelog3Vo.setId(String.valueOf(category3.getCatId()));
                            return catelog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(catelog3Vos);
                        return catelog2Vo;
                    }).collect(Collectors.toList());

                    return catelog2Vos;
                }));

        //3.保存到redis中（在释放锁之前就把数据放到redis中，防止下一个线程再查一次数据库）
        /**
         * 当第一个线程释放锁之后还没来得及保存到redis中时，第二个线程发现redis中没有，又查询了一遍数据库
         * 所以要把保存到redis中的操作放到同步代码块内
         */
        valueOps.set("categoryJson", map);

        return map;
    }


    /**
     * @version 1.0
     * 使用synchronized锁
     */
    public Map<String, List<Catelog2Vo>> getCategoryJsonFromDb() {

        /**
         * 1.使用synchronized(或者JUC中的ReentrantLock)锁：
         *   同步监视器是this，由于SpringBoot的组件是单例模式，所以this是多个线程共享的对象
         * ❗️注意：多线程并发访问下，有可能上一个线程执行完后缓存中已经有数据了，
         *          但是当前线程已经执行到从mysql中查询这一步，所以需要再从缓存中查一次，再次检验，减少对mysql的访问量
         *⚡️缺点：本地锁(当前进程锁)只能锁住当前服务，分布式系统下有多个商品服务，使用本地锁就会产生多个本地锁，
         *          就会查询多次数据库，导致数据库压力增大
         * 2.使用分布式锁：
         * ♥️优缺点：可以同时锁住多个服务，只查询一次数据库，数据库压力减小，但是性能速度慢
         */
        synchronized (this) {
            return getDataFromDb();
        }

    }

    /**
     * @version 2.0
     * 使用redis setnx 作为分布式锁，在redis中占位（锁住）
     */
    public Map<String, List<Catelog2Vo>> getCategoryJsonFromDbWithRedisLock() {


        /**
         * 1.使用synchronized(或者JUC中的ReentrantLock)锁：
         *   同步监视器是this，由于SpringBoot的组件是单例模式，所以this是多个线程共享的对象
         * ❗️注意：多线程并发访问下，有可能上一个线程执行完后缓存中已经有数据了，
         *          但是当前线程已经执行到从mysql中查询这一步，所以需要再从缓存中查一次，再次检验，减少对mysql的访问量
         *⚡️缺点：本地锁(当前进程锁)只能锁住当前服务，分布式系统下有多个商品服务，使用本地锁就会产生多个本地锁，
         *          就会查询多次数据库，导致数据库压力增大
         * 2.使用分布式锁：
         *   在redis中先进行占位(获取锁)，方法执行完毕后删除占位(释放锁)
         * ♥️优缺点：可以同时锁住多个服务，只查询一次数据库，数据库压力减小，但是性能速度慢
         */

        //设置过期时间300秒(防止业务时间过长，所以过期时间也设置长一点)
        //即使业务代码出现异常或者物理异常，300秒后会自动释放锁
        String uuid = UUID.randomUUID().toString();
        //redis的setnx
        Boolean lock = valueOps.setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        //if (lock){
        if (Boolean.TRUE.equals(lock)) {//如果获得到了锁（占位成功）
            System.out.println("获取分布式锁成功...");

            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                //执行完方法后释放锁(删除占位)
                //redisTemplate.delete("lock");
                //通过使用lua脚本进行原子性删除
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then return redis.call('del',KEYS[1]) " +
                        "else return 0 end";
                //释放锁(删除占位符)
                Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }

            return dataFromDb;//从数据库中获取数据(会再次验证redis中是否含有数据)
        } else {
            System.out.println("获取分布式锁失败...等待重试");

            // 加锁失败，重试， synchronized()是自旋锁，自动重试
            // 休眠200ms重试
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //手动重试，自旋锁的方式
            return this.getCategoryJsonFromDbWithRedisLock();
        }

    }

    /**
     * @return
     * @version 3.0
     * 使用Redisson作为分布式锁
     * 问题：缓存中的数据如何与数据库保持一致，缓存一致性问题
     * 解决方式：双写模式，失效模式
     * <p>
     * @version 4.0
     * 使用SpringCache简化缓存操作
     */
    public Map<String, List<Catelog2Vo>> getCategoryJsonFromDbWithRedissonLock() {


        /**
         * 1.使用synchronized(或者JUC中的ReentrantLock)锁：
         *   同步监视器是this，由于SpringBoot的组件是单例模式，所以this是多个线程共享的对象
         * ❗️注意：多线程并发访问下，有可能上一个线程执行完后缓存中已经有数据了，
         *          但是当前线程已经执行到从mysql中查询这一步，所以需要再从缓存中查一次，再次检验，减少对mysql的访问量
         *⚡️缺点：本地锁(当前进程锁)只能锁住当前服务，分布式系统下有多个商品服务，使用本地锁就会产生多个本地锁，
         *          就会查询多次数据库，导致数据库压力增大
         * 2.使用redis setnx锁：
         *   在redis中先进行占位(获取锁)，方法执行完毕后删除占位(释放锁)
         * ♥️优缺点：可以同时锁住多个服务，只查询一次数据库，数据库压力减小，但是性能速度慢
         * 3.使用Redisson分布式锁：
         *  优点：1.Redisson可以理解为分布式版的JUC，底层使用Lua脚本，所有操作都是原子性的
         *       2.加锁失败会进入阻塞状态，会自动重试，相比于redis的setnx不用手动重试
         *
         */

        RLock catelogJsonLock = redissonClient.getLock("catelogJson-lock");
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            //具有看门狗机制，锁过期会自动续期
            boolean tryLock = catelogJsonLock.tryLock(100, TimeUnit.SECONDS);
            if (tryLock) {
                System.out.println("获取分布式锁成功...");
                dataFromDb = getDataFromDb();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            catelogJsonLock.unlock();//手动释放锁，防止看门狗机制bug导致死锁
        }

        return dataFromDb;//从数据库中获取数据(会再次验证redis中是否含有数据)

    }


    /**
     * @version 4.0
     * 使用SpringCache代替RedisTemplate手动存Redis缓存的操作
     * 改进后：直接从数据库中查询数据，不用手动再去Redis中存数据
     */
    public Map<String, List<Catelog2Vo>> getDataFromDbSpringCache() {
        /**
         * 优化：
         * 1.将数据库的多次查询变为一次
         */
        List<Category> categories = this.baseMapper.selectList(null);
        System.out.println("getCategoryJsonFromDb()从mysql中获取到数据！");


        //1.查询出所有一级分类
        List<Category> listLevel1 = getChildren(categories, 0L);

        //2.根据查询一级分类对应的二级和三级分类，并设置vo
        Map<String, List<Catelog2Vo>> map = listLevel1.stream().collect(Collectors.toMap(category1 -> category1.getCatId().toString(),
                category1 -> {
                    //查询一级分类对应的所有二级分类
                    List<Category> listLevel2 = getChildren(categories, category1.getCatId());
                    List<Catelog2Vo> catelog2Vos = listLevel2.stream().map(category2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo();
                        catelog2Vo.setCatalog1Id(String.valueOf(category1.getCatId()));
                        catelog2Vo.setName(category2.getName());
                        catelog2Vo.setId(String.valueOf(category2.getCatId()));
                        //查询二级分类对应的所有三级分类
                        List<Category> listLevel3 = getChildren(categories, category2.getCatId());
                        List<Catelog3Vo> catelog3Vos = listLevel3.stream().map(category3 -> {
                            Catelog3Vo catelog3Vo = new Catelog3Vo();
                            catelog3Vo.setName(category3.getName());
                            catelog3Vo.setCatalog2Id(String.valueOf(category2.getCatId()));
                            catelog3Vo.setId(String.valueOf(category3.getCatId()));
                            return catelog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(catelog3Vos);
                        return catelog2Vo;
                    }).collect(Collectors.toList());

                    return catelog2Vos;
                }));

        /*//3.保存到redis中（在释放锁之前就把数据放到redis中，防止下一个线程再查一次数据库）
         *//**
         * 当第一个线程释放锁之后还没来得及保存到redis中时，第二个线程发现redis中没有，又查询了一遍数据库
         * 所以要把保存到redis中的操作放到同步代码块内
         *//*
        valueOps.set("categoryJson", map);*/

        return map;
    }


    /**
     * 优化：从redis中获取数据
     */
    @Override
    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    public Map<String, List<Catelog2Vo>> getCategoryJson() {
        //1.先从redis中查询
        Map<String, List<Catelog2Vo>> categoryJson = (Map<String, List<Catelog2Vo>>) valueOps.get("categoryJson");
        if (categoryJson != null && categoryJson.size() > 0) {
            System.out.println("getCategoryJson()从redis中获取到数据！");
            return categoryJson;
        } else {
            //2.从mysql中查询(getDataFromDb()方法中会再次检验redis中是否含有数据)
            //Map<String, List<Catelog2Vo>> categoryJsonFromDb = getCategoryJsonFromDbWithRedissonLock();
            Map<String, List<Catelog2Vo>> categoryJsonFromDb = getDataFromDbSpringCache();

            //3.保存到redis中(更改到在getDataFromDb()方法的锁内进行，
            // 在锁释放之前就把数据存到redis中，防止下一个线程还会从mysql中查询)
            /**
             * 当第一个线程释放锁之后还没来得及保存到redis中时，第二个线程发现redis中没有，又查询了一遍数据库
             * 所以要把保存到redis中的操作使用锁锁住(最终方案：使用Redisson分布式锁)
             */

            return categoryJsonFromDb;
        }

    }

}
